<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
    
  <meta name="description" content="welcome" />
  
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    JVM垃圾回收(GC) |  ChenyyのBlog
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="/favicon.ico" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">
  
<link rel="stylesheet" href="/css/custom.css">

  
  <script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>
  
  

  

</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-Java/JVM垃圾回收"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  JVM垃圾回收(GC)
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/2021/06/05/Java/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/" class="article-date">
  <time datetime="2021-06-05T12:21:58.000Z" itemprop="datePublished">2021-06-05</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/Java/">Java</a>
  </div>
  
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> 字数统计:</span>
            <span class="post-count">6.6k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> 阅读时长≈</span>
            <span class="post-count">24 分钟</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <ul>
<li>如何识别垃圾</li>
<li>垃圾回收主要方法</li>
<li>分代收集算法</li>
<li>垃圾收集器</li>
<li>JVM参数</li>
<li>测试</li>
</ul>
<span id="more"></span>

<h1 id="如何识别垃圾"><a href="#如何识别垃圾" class="headerlink" title="如何识别垃圾"></a><strong>如何识别垃圾</strong></h1><h2 id="引用计数法"><a href="#引用计数法" class="headerlink" title="引用计数法"></a>引用计数法</h2><p>对象被引用一次，在它的对象头上加一次引用次数，如果没有被引用（引用次数为 0），则此对象可回收</p>
<p>代码 ref 引用了右侧定义的对象，所以引用次数是 1</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">String ref = new String(&quot;Java&quot;);</span><br></pre></td></tr></table></figure>

<p>如果在上述代码后面添加一个 ref = null，则由于对象没被引用，引用次数置为 0，由于不被任何变量引用，此时即被回收。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/640.gif"></p>
<p>引用计数法无法解决一个主要的问题就是循环引用。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">// 第一步</span><br><span class="line">A a = new TestRC(&quot;a&quot;);</span><br><span class="line">B b = new TestRC(&quot;b&quot;);</span><br><span class="line">// 第二步</span><br><span class="line">a.instance = b;</span><br><span class="line">b.instance = a;</span><br><span class="line">// 第三步</span><br><span class="line">a = null;</span><br><span class="line">b = null;</span><br></pre></td></tr></table></figure>

<p>虽然 a，b 都被置为 null 了，但是由于之前它们指向的对象互相指向了对方（引用计数都为 1），所以无法回收，也正是由于无法解决循环引用的问题，所以现代虚拟机都不用引用计数法来判断对象是否应该被回收。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210602153835.png"></p>
<h2 id="标记算法-可达性算法"><a href="#标记算法-可达性算法" class="headerlink" title="标记算法/可达性算法"></a>标记算法/可达性算法</h2><p>可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发，引出它们指向的下一个节点，直到所有的结点都遍历完毕，串成一条引用链，如果对象不在任意一个引用链中，则这些对象会被判断为「垃圾」，会被 GC 回收。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210602154055.png"></p>
<h3 id="哪些对象是gc-root？"><a href="#哪些对象是gc-root？" class="headerlink" title="哪些对象是gc root？"></a>哪些对象是gc root？</h3><p>1、在虚拟机栈中引用的对象（栈帧中的本地变量表），即当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">public class Test &#123;</span><br><span class="line">    public static  void main(String[] args) &#123;</span><br><span class="line">  Test a = new Test();</span><br><span class="line">  // 原来指向的实例 new Test() 会被回收。</span><br><span class="line">  a = null;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>2、在方法区中类静态属性引用的对象，例如java类的引用类型静态变量。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">public  class Test &#123;</span><br><span class="line">    public  static Test s;</span><br><span class="line">    public static  void main(String[] args) &#123;</span><br><span class="line">  Test a = new Test();</span><br><span class="line">  // 类静态属性引用s，指向的对象new Test()依然存活。</span><br><span class="line">  a.s = new Test();</span><br><span class="line">  // 原来指向的实例 new Test() 会被回收。</span><br><span class="line">  a = null;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>3、在方法区中常量引用的对象，如String字符串常量池里的引用。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">public  class Test &#123;</span><br><span class="line">  // 常量s指向的对象不会因为 a指向的对象被回收而回收</span><br><span class="line">  public  static  final Test s = new Test();</span><br><span class="line">        public static void main(String[] args) &#123;</span><br><span class="line">      Test a = new Test();</span><br><span class="line">      a = null;</span><br><span class="line">        &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>4、Java虚拟机内部的引用，如基本数据类型对应的class对象，一些异常对象(比如NPE,OOM)等，还有类加载器。</p>
<p>5、所有被synchronized锁住的对象引用。</p>
<p>6、本地方法栈中 JNI（Native 方法）引用的对象。</p>
<h3 id="对象可回收，就一定会被回收吗"><a href="#对象可回收，就一定会被回收吗" class="headerlink" title="对象可回收，就一定会被回收吗?"></a>对象可回收，就一定会被回收吗?</h3><p>对象的 finalize 方法给了对象一次垂死挣扎的机会，当对象不可达（可回收）时，当发生GC时，会先判断对象是否执行了 finalize 方法，如果未执行，则会先执行 finalize 方法，我们可以在此方法里将当前对象与 GC Roots 关联，这样执行 finalize 方法之后，GC 会再次判断对象是否可达，如果不可达，则会被回收，如果可达，则不回收！</p>
<p>注意： finalize 方法只会被执行一次，如果第一次执行 finalize 方法此对象变成了可达确实不会回收，但如果对象再次被 GC，则会忽略 finalize 方法，对象会被回收！这一点切记!</p>
<h1 id="对象引用与垃圾回收的关系"><a href="#对象引用与垃圾回收的关系" class="headerlink" title="对象引用与垃圾回收的关系"></a>对象引用与垃圾回收的关系</h1><p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604154450.png"></p>
<h2 id="强引用"><a href="#强引用" class="headerlink" title="强引用"></a>强引用</h2><ul>
<li>最常见的对象：通过new关键字创建，通过GC Root能找到的对象。</li>
<li>当所有的GC Root都不通过【强引用】引用该对象时，对象才能被垃圾回收</li>
</ul>
<h2 id="软引用"><a href="#软引用" class="headerlink" title="软引用"></a>软引用</h2><ul>
<li>仅有【软引用】引用该对象时，在垃圾回收后，内存仍不足时会再次发起垃圾回收，回收软引用对象</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">创建一个软引用：SoftReference ref = new SoftReference&lt;&gt;(new Object());</span><br></pre></td></tr></table></figure>

<ul>
<li><p>软引用被回收后，仍然还保留一个null，如将软引用加入集合，回收后遍历集合仍然还存在一个null</p>
</li>
<li><p>解决：使用引用队列，软引用关联的对象被回收时，软引用自身会被加入到引用队列中，通过queue.poll()取得对象进行删除。</p>
</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">创建一个而引用队列：ReferenceQueue queue = new ReferenceQueue&lt;&gt;();</span><br><span class="line"></span><br><span class="line">创建加入了引用队列的软引用：SoftReference ref = new SoftReference&lt;&gt;(new Object(),queue);</span><br></pre></td></tr></table></figure>



<h2 id="弱引用"><a href="#弱引用" class="headerlink" title="弱引用"></a>弱引用</h2><ul>
<li>仅有【弱引用】引用该对象时，在垃圾回收时，无论内存是否充足，都会回收弱引用对象。</li>
<li>可以配合引用队列来释放弱引用自身。引用队列使用同软引用。</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">创建一个弱引用：WeakReference ref = new WeakReference&lt;&gt;(new Object());</span><br></pre></td></tr></table></figure>



<h2 id="虚引用"><a href="#虚引用" class="headerlink" title="虚引用"></a>虚引用</h2><p>必须配合引用队列使用，主要配合ByteBuffer使用，被引用对象回收时，会将【虚引用】入队，由Reference Hanler线程调用虚引用相关方法释放【直接内存】（unsafe类中方法）</p>
<h2 id="终结器引用"><a href="#终结器引用" class="headerlink" title="终结器引用"></a>终结器引用</h2><p>无需手动编码，但其内部配合引用队列使用，在垃圾回收时，终结器引用队列入队（引用对象暂未回收），再由Finalizer线程通过终结器引用找到被引用对象并调用他的finalize方法，第二次gc时回收被引用对象</p>
<h1 id="垃圾回收主要方法"><a href="#垃圾回收主要方法" class="headerlink" title="垃圾回收主要方法"></a><strong>垃圾回收主要方法</strong></h1><h2 id="标记清除算法"><a href="#标记清除算法" class="headerlink" title="标记清除算法"></a>标记清除算法</h2><p>步骤：</p>
<ul>
<li>先根据可达性算法标记出相应的可回收对象（图中黄色部分）</li>
<li>对可回收的对象进行回收</li>
</ul>
<p>优点：处理速度快。</p>
<p>缺点：造成空间不连续，产生内存碎片。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603215918.png"></p>
<h2 id="复制算法"><a href="#复制算法" class="headerlink" title="复制算法"></a>复制算法</h2><p>步骤：</p>
<ul>
<li>分配同等大小的内存空间</li>
<li>标记被GC Root引用的对象</li>
<li>将引用的对象连续复制到另一块内存空间</li>
<li>清除原来的内存空间</li>
</ul>
<p>优点：空间连续，没有内存碎片。</p>
<p>缺点：存在空间浪费，有一半空间未使用。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210602182157.png"></p>
<h2 id="标记整理法"><a href="#标记整理法" class="headerlink" title="标记整理法"></a>标记整理法</h2><ul>
<li>标记没有被GC Root引用的对象</li>
<li>整理被引用的对象</li>
<li>将所有的存活对象都往一端移动，紧邻排列</li>
<li>再清理掉另一端的所有区域</li>
</ul>
<p>优点：空间连续，没有内存碎片，空间利用率高。</p>
<p>缺点：整理导致效率较低。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603220440.png"></p>
<h1 id="分代收集算法"><a href="#分代收集算法" class="headerlink" title="分代收集算法"></a>分代收集算法</h1><h2 id="为什么要分代"><a href="#为什么要分代" class="headerlink" title="为什么要分代"></a>为什么要分代</h2><p>1、分代回收可以对堆中对象采用不同的gc策略。</p>
<p>大部分的对象都很短命，都在很短的时间内都被回收了（98% 的对象都是朝生夕死的，经过一次 Minor GC 后就会被回收。</p>
<p>分代收集算法根据对象存活周期的不同将堆分成新生代和老生代，默认比例为 1 : 2，新生代又分为 Eden 区， from Survivor 区（简称S0），to Survivor 区(简称 S1)，三者的比例为 8: 1 : 1。</p>
<p>根据新老生代的特点选择最合适的垃圾回收算法，我们把新生代发生的 GC 称为 Young GC（也叫 Minor GC）,老年代发生的 GC 称为 Old GC（也称为 Full GC或者Major GC）。</p>
<p>2、分代以后，gc时进行可达性分析的范围能大大降低。</p>
<p>在分代回收中，新生代的规模比老年代小，回收频率也要高，显然新生代gc的时候不能去遍历老年代。</p>
<p>这时候只要把非收集部分指向收集部分的引用保存下来，加入gc roots，就可以避免在新生代gc时去对老年代进行可达性分析（再次注意老年代对象大量存活），能节省大量时间。</p>
<p>而如果不进行分代，由于老年代对象长期存活，所以总的gc频率应该和分代以后的young gc差不多，但是每次gc都需要从gc roots进行完整的堆遍历，无疑大大增加了开销。</p>
<h2 id="分代垃圾回收流程"><a href="#分代垃圾回收流程" class="headerlink" title="分代垃圾回收流程"></a>分代垃圾回收流程</h2><h3 id="对象在新生代的分配与回收"><a href="#对象在新生代的分配与回收" class="headerlink" title="对象在新生代的分配与回收"></a>对象在新生代的分配与回收</h3><p>对象首先分配在Eden伊甸园区域，大部分对象在很短的时间内都会被回收。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603222433.png"></p>
<p>当 Eden 区将满时，触发 Young GC（也叫 Minor GC）。Minor GC会引发Stop the world（STW）现象，暂停其他用户的线程。垃圾回收结束后，用户线程才恢复运行。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603222535.png"></p>
<p>存活对象使用<strong>复制算法</strong>移到 S0 区（from Survivor 区），同时对象年龄加一，再把 Eden 区对象全部清理以释放出空间。</p>
<p><img src="https://mmbiz.qpic.cn/mmbiz_gif/OyweysCSeLUrYqPicjVwjuMChPrPicNHdXdZ6EYjicg7nuibII5xZBwiaGq2S454iaFRibc5Z1ibiaLKnYyldeQyzAO9ibRg/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1" alt="å¾ç"></p>
<p>当触发下一次 Minor GC 时，会把 Eden 区的存活对象和 S0（from Survivor 区） 中的存活对象复制到 S1（to Survivor 区），同时存活对象年龄+1，并清空 Eden 和 S0 的空间，再交换S1区和S0区。</p>
<p><img src="https://mmbiz.qpic.cn/mmbiz_gif/OyweysCSeLUrYqPicjVwjuMChPrPicNHdXlN3YAWPibCzl5Lw4ZWKHAU8f2YZFEO3ugjYbQ4fSfUCFyU2Mmp904kg/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1" alt="å¾ç"></p>
<h3 id="新生代对象晋升老年代"><a href="#新生代对象晋升老年代" class="headerlink" title="新生代对象晋升老年代"></a>新生代对象晋升老年代</h3><p>1、当对象寿命超过阈值时，会晋升至老年代（默认15岁2^4，CMS收集器默认6岁），可以通过参数<code>-XX:MaxTenuringThreshold </code>来设置。</p>
<p><img src="https://mmbiz.qpic.cn/mmbiz_gif/OyweysCSeLUrYqPicjVwjuMChPrPicNHdXfflQYTnxmA64gbBCogBQncpxu0AumticAib02Cv8oEdafymtcVSwPBQQ/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1" alt="å¾ç"></p>
<p>2、大对象会直接晋升到老年代。分配在 Eden 区，复制慢，占空间。JVM参数<code>-XX:PretenureSizeThreshold</code>可以设置大对象的大小(单位字节)，如果对象超过设置大小会直接进入老年代，不会进入年轻代，这个参数只在 Serial 和ParNew两个收集器下有效。</p>
<p>3、动态对象年龄判断。大于设置的动态年龄阈值的对象都会进入老年代，从1岁+2岁+…+n岁对象大小累加，大于survior区50%，以n岁作为阈值，大于等于这个年龄的对象都会进入老年代。通过参数设置：<code>-XX:TargetSurvivorRatio=50</code></p>
<h3 id="空间分配担保"><a href="#空间分配担保" class="headerlink" title="空间分配担保"></a>空间分配担保</h3><p>是否设置空间分配担保的JVM参数：<code>-XX:HandlePromotionFailure</code>。</p>
<p>在发生 MinorGC 之前，虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。</p>
<ul>
<li>如果老年代最大可用连续空间大于新生代总对象空间，那么Minor GC 可以确保是安全的。</li>
<li>如果不大于，那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。</li>
<li>如果允许担保失败，那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小。</li>
<li>如果老年代最大可用连续空间大于历代晋升对象，则进行 Minor GC，否则进行一次 Full GC。</li>
</ul>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604162608.png"></p>
<h3 id="Full-GC-amp-amp-Stop-The-World"><a href="#Full-GC-amp-amp-Stop-The-World" class="headerlink" title="Full GC &amp;&amp; Stop The World"></a>Full GC &amp;&amp; Stop The World</h3><p>当存放大对象新生代和老年代都放不下时，抛出OOM异常。</p>
<p>当老年代空间不足，会先尝试触发Minor GC，如果之后空间仍不足，会触发Full GC。</p>
<p>Full GC 会同时回收新生代和老年代（老年代使用标签清除或标记整理算法），它会导致 Stop The World（简称 STW）。</p>
<p>什么是 STW ？所谓的 STW, 即在 GC（minor GC 或 Full GC）期间，只有垃圾回收器线程在工作，其他工作线程则被挂起。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603224627.png"></p>
<p>GC会选在 Safe Point进行，一般在循环的末尾、方法返回前、调用方法的 call 之后、抛出异常的位置。</p>
<p>Eden, S0，S1设置为8:1:1，新生代与老年代默认设置成 1:2 ，都是为了尽可能地避免对象过早地进入老年代，尽可能晚地触发 Full GC。</p>
<h1 id="垃圾收集器"><a href="#垃圾收集器" class="headerlink" title="垃圾收集器"></a>垃圾收集器</h1><ul>
<li>在新生代工作的垃圾回收器：Serial, ParNew, ParallelScavenge</li>
<li>在老年代工作的垃圾回收器：CMS，Serial Old, Parallel Old</li>
<li>同时在新老生代工作的垃圾回收器：G1</li>
</ul>
<p>存在连线的垃圾收集器，代表它们之间可以配合使用。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603225841.png"></p>
<h2 id="新生代收集器"><a href="#新生代收集器" class="headerlink" title="新生代收集器"></a>新生代收集器</h2><h3 id="Serial-串行收集器"><a href="#Serial-串行收集器" class="headerlink" title="Serial 串行收集器"></a><strong>Serial 串行收集器</strong></h3><ul>
<li>命令：<code>-XX:+UseSerialGC </code></li>
<li>特点：复制算法，Client模式下默认的年轻代收集器。</li>
</ul>
<p>Serial串行收集器是单线程的垃圾收集器，它在进行垃圾收集时，其他用户线程会暂停，直到垃圾收集结束。</p>
<p>在Client 模式下，它简单有效。对于单CPU 环境来说，Serial 单线程模式无需与其他线程交互，内存不大时，STW 时间可以控制在毫秒级别。因此对于Client 模式下的虚拟机，Serial 收集器是新生代的默认收集器。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604144554.png"></p>
<h3 id="ParNew并行收集器（CMS）"><a href="#ParNew并行收集器（CMS）" class="headerlink" title="ParNew并行收集器（CMS）"></a><strong>ParNew并行收集器（CMS）</strong></h3><ul>
<li>命令：<code>-XX:UseParNewGC</code>，设定并行垃圾收集的线程数量（默认开启的线程数等于cpu数）<code>-XX:ParalletGCThreads</code>。</li>
<li>特点：Serial 的多线程版，使用复制算法。并行收集，尽可能缩短单次GC的停顿时间。</li>
</ul>
<p>ParNew（Parallel  New） 收集器是 Serial 收集器的多线程版本，多核环境较Serial效率高，除了使用多线程，其他的收集算法、对象分配规则、回收策略与 Serial 收集器完成一样。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603230005.png"></p>
<p>ParNew 主要工作在 Server 模式，多线程能让垃圾回收得更快，减少了 STW 时间，也是许多运行在 Server 模式下的虚拟机的首选新生代收集器。</p>
<p>除了 Serial  收集器，只有ParNew能与 CMS 收集器配合工作，他们底层共用一套代码框架。CMS 是真正意义上的并发收集器，它第一次实现了垃圾收集线程与用户线程（基本上）同时工作。</p>
<h3 id="Parallel-Scavenge-自适应并行收集器"><a href="#Parallel-Scavenge-自适应并行收集器" class="headerlink" title="Parallel Scavenge 自适应并行收集器"></a><strong>Parallel Scavenge 自适应并行收集器</strong></h3><ul>
<li>命令：<code>-XX:+UseParallelGC</code></li>
<li>特点：复制算法，自适应策略控制吞吐量，Server模式下默认的年轻代垃圾回收器。</li>
</ul>
<p><strong>自适应策略是 Parallel Scavenge  与 ParNew 的重要区别。</strong></p>
<p>Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量：</p>
<ul>
<li>最大垃圾收集时间（单位毫秒）<code>-XX:MaxGCPauseMillis</code>。</li>
<li>吞吐量占比（默认99%）<code> -XX:GCTimeRatio</code>。GC最大花费时间的比率=1/(1+99)=1%，程序每运行100分钟，允许GC停顿共1分钟，其吞吐量=1-GC最大花费时间比率=99%</li>
<li>开启<code> -XX:UseAdaptiveSizePolicy</code>，就不需要设置<code>-Xmn、-XX:SurvivorRatio</code>，虚拟机就会根据系统运行情况动态调整参数，以达到设定的垃圾收集时间或吞吐量指标。</li>
</ul>
<p>它跟ParNew的关注点不同，CMS 、ParNew垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间，而 Parallel Scavenge 目标是达到可控制的吞吐量（吞吐量 = 运行用户代码时间 / （运行用户代码时间+垃圾收集时间））。也就是说 ParNew 等垃圾收集器更适合用到与用户交互的程序，因为停顿时间越短，用户体验越好，而 Parallel Scavenge 收集器关注的是吞吐量，所以更适合做后台运算等不需要太多用户交互的任务。</p>
<h2 id="老年代收集器"><a href="#老年代收集器" class="headerlink" title="老年代收集器"></a>老年代收集器</h2><h3 id="Serial-Old-串行收集器"><a href="#Serial-Old-串行收集器" class="headerlink" title="Serial Old 串行收集器"></a><strong>Serial Old 串行收集器</strong></h3><ul>
<li>命令：<code>-XX:+UseSerialOldGC </code></li>
<li>特点：标记-整理算法，Client模式下默认的老年代收集器。</li>
</ul>
<p>上文我们知道， Serial 收集器是工作于新生代的单线程收集器，与之相对地，Serial Old 是工作于老年代的单线程收集器，此收集器的主要意义在于给 Client 模式下的虚拟机使用。</p>
<p>如果在 Server 模式下，则它还有两大用途：</p>
<ul>
<li>一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用。</li>
<li>另一种是作为 CMS 收集器的后备预案，在并发收集发生 Concurrent Mode Failure 时使用。</li>
</ul>
<p>Serial Old与 Serial 收集器配合使用示意图如下</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603230119.png"></p>
<h3 id="Parallel-Old-并行收集器（吞吐量）"><a href="#Parallel-Old-并行收集器（吞吐量）" class="headerlink" title="Parallel Old 并行收集器（吞吐量）"></a><strong>Parallel Old 并行收集器（吞吐量）</strong></h3><ul>
<li>命令：<code>-XX:+UseParallelOldGC</code></li>
<li>特点：使用标记整理算法，多线程并行执行，配合Parallel Scavenge实现吞吐量优先。</li>
</ul>
<p>Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本，使用多线程和标记整理法，两者的组合由于都是多线程收集器，真正实现了「吞吐量优先」的目标。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604094722.png"></p>
<h3 id="CMS并发标记清除收集器（停顿时间）"><a href="#CMS并发标记清除收集器（停顿时间）" class="headerlink" title="CMS并发标记清除收集器（停顿时间）"></a><strong>CMS并发标记清除收集器（停顿时间）</strong></h3><ul>
<li>命令：<code>-XX:+UseConcMarkSweepGC</code></li>
<li>老年代占用%空间后回收<code>-XX:CMSInitiatingOccupancyFraction</code> </li>
<li>开启碎片整理<code>XX:+UseCMSCompactAtFullCollection</code>。多少次full gc之后整理<code>-XX:CMSFullGCsBeforeCompation</code>。</li>
<li>特点：标记清除算法，与用户线程并发交替执行，响应时间优先，目标实现最短STW停顿时间。</li>
</ul>
<p>并发与并⾏的概念 </p>
<ul>
<li>并⾏（Parallel）：指多条垃圾收集线程并⾏⼯作，但此时⽤户线程仍然处于等待状态，如Parallel Old 收集器。</li>
<li>并发（Concurrent）：指⽤户线程与垃圾收集线程同时执⾏（但不⼀定是并⾏的，可能 会交替执⾏），⽤户程序在继续运⾏，⽽垃圾收集程序运⾏于另⼀个 CPU 上，如CMS收集器。</li>
</ul>
<p>CMS (Concurrent Mark Sweep) 收集器是以<strong>实现最短 STW 时间为目标的收集器，采用的是标记清除法</strong>，主要有以下四个步骤：</p>
<ul>
<li>初始标记(STW)：仅标记 GC Roots 能关联的对象，速度很快。</li>
<li>并发标记：进行 GC Roots  Tracing 的过程，跟用户线程并发执行。</li>
<li>重新标记(STW)：修正并发标记期间，用户线程运行导致的标记变动。</li>
<li>并发清除：清除垃圾对象，不会阻塞用户线程。</li>
</ul>
<p>初始标记和重新标记两个阶段会发生 STW（造成用户线程挂起）。重新标记一般比初始标记耗时长，但远比并发标记时间短。</p>
<p>整个过程中耗时最长的是并发标记和标记清理，不过这两个阶段用户线程都可工作，所以不影响应用的正常使用。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604095010.png"></p>
<p>但是官方一直没有把 CMS 设为默认收集器，甚至在JDK14以后废弃使用，主要有以下三个缺点：</p>
<p><strong>1、CMS 收集器对 CPU 资源敏感，垃圾回收占用的线程会导致吞吐量下降。y</strong></p>
<p>CMS 默认启动的回收线程数是 （CPU数量+3）/ 4。如果有 10 个用户线程处理请求，GC时就需要分出 3 个作为回收线程，吞吐量下降了30%。如果只有一两个线程，那吞吐量将直接下降 50%。</p>
<p><strong>2、CMS 无法处理并发清理阶段产生的浮动垃圾（Floating Garbage）。并发清理阶段空间不足可能出现 <code>「Concurrent Mode Failure」</code>而导致另一次 Full GC 的产生。</strong></p>
<p>由于在并发清理阶段用户线程还在运行，所以清理的同时新的垃圾也在不断出现，这部分垃圾只能在下一次 GC 时再清理掉（即浮动垃圾）。</p>
<p>同时在垃圾收集阶段用户线程也要继续运行，就需要预留足够多的空间要确保用户线程正常执行，这就意味着 CMS 收集器不能像其他收集器一样等老年代满了再使用。</p>
<p>可以通过 <code>-XX:CMSInitiatingOccupancyFraction</code> 来设置老年代使用了多少%空间后进行垃圾回收。但是如果设置地太高很容易导致在 CMS 运行期间预留的内存无法满足程序要求，会导致 Concurrent Mode Failure 失败，这时会启用 Serial Old 单线程收集器来重新进行老年代的收集。</p>
<p><strong>3、CMS 采用的是标记清除法，会产生大量的内存碎片，给大内存分配带来麻烦。</strong></p>
<p>如果无法找到足够大的连续空间来分配对象，将会触发 Full GC，这会影响应用的性能。当然我们可以开启<code> -XX:+UseCMSCompactAtFullCollection</code>（默认是开启的），用于在 CMS 收集器进行 FullGC 时开启内存碎片的合并整理过程。还可以配合另一个参数<code>-XX:CMSFullGCsBeforeCompation</code>用来设置执行多少次full gc之后进行空间整理，默认是0次即每次都整理。</p>
<h3 id="G1（Garbage-First）-收集器"><a href="#G1（Garbage-First）-收集器" class="headerlink" title="G1（Garbage First） 收集器"></a><strong>G1（Garbage First） 收集器</strong></h3><h4 id="特点"><a href="#特点" class="headerlink" title="特点"></a>特点</h4><ul>
<li>命令：<code>-XX:+UseG1GC</code></li>
<li>指定期望停顿时间<code>-XX:MaxGCPauseMillis</code></li>
<li>特点：将Java堆划分为多个大小相等的不连续区域（Region）。整体上是标记整理，区域之间是复制算法，注重吞吐量和低延迟。</li>
</ul>
<p>G1 收集器是面向服务端的垃圾收集器， Java8中Parallel GC是默认的垃圾收集器，在Java 9已经将G1作为默认的垃圾收集器，G1主要有以下几个特点：</p>
<ul>
<li>并行：与CMS 一样，能与应用程序线程并发执行。</li>
<li>跟CMS相比：避免牺牲大量的吞吐性能，不需要更大的 Java Heap。</li>
</ul>
<ul>
<li>空间整理没有内存碎片：G1 从整体上看采用的是标记-整理法，局部（两个 Region）上看是基于复制算法实现的，收集后提供规整的可用内存，有利于程序的长时间运行。</li>
<li>可预测的停顿：在 STW 上建立了可预测的停顿时间模型，通过参数<code>-XX:MaxGCPauseMillis</code>指定期望停顿时间，G1 会将停顿时间控制在用户设定的停顿时间以内。</li>
</ul>
<h4 id="空间分配"><a href="#空间分配" class="headerlink" title="空间分配"></a>空间分配</h4><p>为什么G1能建立可预测的停顿模型呢，主要原因在于 G1 对堆空间的分配与传统的垃圾收集器不一器，传统的内存分配是连续的，分成新生代，老年代，新生代又分 Eden、S0、S1。而 G1 各代的存储地址不是连续的，每一代都使用了 n 个不连续的大小相同的 Region，每个Region占有一块连续的虚拟内存地址。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604143624.png"></p>
<p>Region还多了一个H，它代表Humongous，这表示这些Region存储的是巨大对象（humongous object，H-obj），即大小大于等于region一半的对象，这样超大对象就直接分配到了老年代，防止了反复拷贝移动。</p>
<p>传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集，而分配成各个 Region 之后，方便 G1 跟踪各个 Region 里垃圾堆积的价值大小（回收所获得的空间大小及回收所需经验值），这样根据价值大小维护一个优先列表，根据允许的收集时间，优先收集回收价值最大的 Region，也就避免了整个老年代的回收，也就减少了 STW 造成的停顿时间。同时由于只收集部分 Region，可就做到了 STW 时间的可控。</p>
<p>G1将Java堆划分为多个大小相等的不连续区域（Region），JVM目标是不超过2048个Region(JVM源码里TARGET_REGION_NUMBER 定义)，实际可以超过该值，但是不推荐。</p>
<p>一般Region大小等于堆大小除以2048，比如堆大小为4096M，则Region大小为2M，当然也可以用参数<code>-XX:G1HeapRegionSize</code>。</p>
<h4 id="步骤"><a href="#步骤" class="headerlink" title="步骤"></a>步骤</h4><p>G1 收集器的整体过程与 CMS 收集器非常类似，筛选阶段会根据各个 Region 的回收价值和成本进行排序，根据用户期望的 GC 停顿时间来制定回收计划。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604165013.png"></p>
<p>工作步骤如下：</p>
<ul>
<li>初始标记</li>
<li>并发标记</li>
<li>最终标记</li>
<li>筛选回收</li>
</ul>
<p>老年代内存不足，达到阈值时进入并发标记和混合收集阶段</p>
<ul>
<li>如果回收速度&gt;新产生垃圾的速度 ：并发垃圾收集</li>
<li>如果回收速度&lt;新产生垃圾的速度：串行的full GC</li>
</ul>
<h1 id="JVM参数"><a href="#JVM参数" class="headerlink" title="JVM参数"></a>JVM参数</h1><p>堆</p>
<ul>
<li><code>-Xms</code>堆内存最小值（超过初始值会扩容到最大值），<code>minimum memory size for pile and heap</code>。</li>
<li><code>-Xmx</code>堆内存最大值（通常初始值和最大值一样，因为扩容会导致内存抖动，影响程序运行稳定性），<code>maximum memory size for pile and heap</code>。</li>
<li><code>-Xmn</code>堆新生代的大小，<code>memory size for new generation heap</code>；</li>
<li><code>-XX:NewRatio</code>指定堆中的老年代和新生代的大小比例， 不过使用CMS收集器的时候这个参数会失效。</li>
<li><code>-XX:SurvivorRatio</code>指定Eden 与 Survivor 比例，默认8，即8:1:1。</li>
</ul>
<p>栈</p>
<ul>
<li><code>-Xss</code>设置线程栈的大小（影响并发线程数大小），<code>memory size for stack</code>；</li>
</ul>
<p>元空间</p>
<ul>
<li><code>-XX:MeatspaceSize</code>和<code>-XX:MaxMetaspaceSize</code>，设置方法区的初始大小和最大值，替代了JDK8之前的参数<code>-XX:PermSize</code>和<code>-XX:MaxPermSize</code>。</li>
</ul>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210603225551.png"></p>
<h1 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h1><h2 id="栈溢出StackOverflowError"><a href="#栈溢出StackOverflowError" class="headerlink" title="栈溢出StackOverflowError"></a>栈溢出StackOverflowError</h2><p>设置启动是栈大小，递归调用模拟频繁出入栈。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604155026.png"></p>
<h2 id="长期存活的对象将进入老年代"><a href="#长期存活的对象将进入老年代" class="headerlink" title="长期存活的对象将进入老年代"></a>长期存活的对象将进入老年代</h2><p>设置3岁后进入老年代，JVM参数设置如下： <code>-Xmx300m -Xms300m -Xmn100m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=3 -XX:+PrintGCDateStamps </code></p>
<p>第一次进行Ygc，age0-&gt;1，新生代的部分对象被放到from survivor区，此时老年代used 为0K。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604161053.png"></p>
<p>第二次GC完，age1-&gt;2。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604161201.png"></p>
<p>第三次GC完，age2-&gt;3。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604161225.png"></p>
<p>第四次GC的时候，检测到from区的对象age到达了3，搬到老年代。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604161249.png"></p>
<h2 id="对象动态年龄判断"><a href="#对象动态年龄判断" class="headerlink" title="对象动态年龄判断"></a>对象动态年龄判断</h2><p>超过了Survivor区域的30%会进入老年代，JVM参数设置：<code>-Xmx300m -Xms300m -Xmn100m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=3 -XX:+PrintGCDateStamps -XX:TargetSurvivorRatio=30</code></p>
<p>在eden区GC移动到from区的对象，在两次GC之后 ，s区的容纳了30%，触发了动态年龄判断，直接存入老年代，会导致old区更快触发Full GC</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604161636.png"></p>
<h2 id="大对象直接进入老年代"><a href="#大对象直接进入老年代" class="headerlink" title="大对象直接进入老年代"></a>大对象直接进入老年代</h2><p>设置JVM参数：<code>-Xmx300m -Xms300m -Xmn100m -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC</code></p>
<p>下图左边没有加<code>-XX:PretenureSizeThreshold=1000000</code>(单位是字节) 这个参数的表现。</p>
<p><img src="https://gitee.com/chenyy-2017/pic/raw/master/note/20210604162053.png"></p>
<h2 id="G1，CMS及ParallelOld对比"><a href="#G1，CMS及ParallelOld对比" class="headerlink" title="G1，CMS及ParallelOld对比"></a>G1，CMS及ParallelOld对比</h2><p>JVM配置<code>-Xms256m -Xmx768m -XX:MaxPermSize=256m</code>，总的运行时间是30分钟。使用了三种不同的GC算法：<code>-XX:+UseParallelOldGC</code>，<code>-XX:+UseConcMarkSweepGC</code>，<code>-XX:+UseG1GC</code>。根据远离强行模拟的结果如下：</p>
<table>
<thead>
<tr>
<th></th>
<th>Parallel Old</th>
<th>CMS</th>
<th>G1</th>
</tr>
</thead>
<tbody><tr>
<td>Total GC pauses<br />（总耗时/吞吐量）</td>
<td>14 870</td>
<td>32 000</td>
<td>20 930</td>
</tr>
<tr>
<td>Max GC pause<br />（单次耗时/停顿时间）</td>
<td>721</td>
<td>50</td>
<td>64</td>
</tr>
</tbody></table>
<p>首先来看Parallel Old  (<code>-XX:+UseParallelOldGC</code>)。测试中GC总耗时15秒，最长的延迟时间721毫秒。总的运行时间来看，GC周期减少了0.8%的吞吐量。</p>
<p>下一个CMS（<code>-XX:+UseConcMarkSweepGC</code>）。测试中GC总耗时32秒，相比并行模式更长，吞吐量下降了1.7%，不过最长延迟时间只有50毫秒。</p>
<p>最后G1（<code>-XX:+UseG1GC</code>）。测试结果的吞吐量减少了1.1%，最长的延迟时间64ms。相比之下是一种兼顾吞吐量和停顿时间的 GC 实现。</p>
<p>参考：</p>
<p>看完这篇垃圾回收，和面试官扯皮没问题了：<a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s/UwrSOx4enEX9iNmD4q_dXg">https://mp.weixin.qq.com/s/UwrSOx4enEX9iNmD4q_dXg</a></p>
<p>JVM内存结构和Java内存模型别再傻傻分不清了：<a target="_blank" rel="noopener" href="https://blog.csdn.net/qq_41170102/article/details/104650162">https://blog.csdn.net/qq_41170102/article/details/104650162</a></p>
<p>gc roots有哪些呢？：<a target="_blank" rel="noopener" href="https://www.zhihu.com/question/50381439">https://www.zhihu.com/question/50381439</a></p>
<p>java的gc为什么要分代？：<a target="_blank" rel="noopener" href="https://www.zhihu.com/question/53613423">https://www.zhihu.com/question/53613423</a></p>
<p>G1，CMS及PARALLEL GC的比较：<a target="_blank" rel="noopener" href="http://it.deepinmind.com/gc/2014/05/01/g1-vs-cms-vs-parallel-gc.html">http://it.deepinmind.com/gc/2014/05/01/g1-vs-cms-vs-parallel-gc.html</a></p>
 
      <!-- reward -->
      
      <div id="reword-out">
        <div id="reward-btn">
          打赏
        </div>
      </div>
      
    </div>
    

    <!-- copyright -->
    
    <div class="declare">
      <ul class="post-copyright">
        <li>
          <i class="ri-copyright-line"></i>
          <strong>版权声明： </strong>
          
          本博客所有文章除特别声明外，著作权归作者所有。转载请注明出处！
          
        </li>
      </ul>
    </div>
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=http://example.com/2021/06/05/Java/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Java/" rel="tag">Java</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
      <a href="/2021/06/05/resource/profession/%E4%BA%92%E8%81%94%E7%BD%91%E9%87%91%E8%9E%8D/" class="article-nav-link">
        <strong class="article-nav-caption">上一篇</strong>
        <div class="article-nav-title">
          
            互联网金融
          
        </div>
      </a>
    
    
      <a href="/2021/06/03/cache/%E7%94%B5%E5%95%86%E8%AF%A6%E6%83%85%E9%A1%B5%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84%EF%BC%88%E5%8D%81%E4%B8%89%EF%BC%89redis-cluster%E9%9B%86%E7%BE%A4%E6%A8%A1%E5%BC%8F/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">电商详情页缓存架构（十三）redis-cluster集群模式</div>
      </a>
    
  </nav>

   
<!-- valine评论 -->
<div id="vcomments-box">
  <div id="vcomments"></div>
</div>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/valine@1.4.14/dist/Valine.min.js"></script>
<script>
  new Valine({
    el: "#vcomments",
    app_id: "",
    app_key: "",
    path: window.location.pathname,
    avatar: "monsterid",
    placeholder: "给我的文章加点评论吧~",
    recordIP: true,
  });
  const infoEle = document.querySelector("#vcomments .info");
  if (infoEle && infoEle.childNodes && infoEle.childNodes.length > 0) {
    infoEle.childNodes.forEach(function (item) {
      item.parentNode.removeChild(item);
    });
  }
</script>
<style>
  #vcomments-box {
    padding: 5px 30px;
  }

  @media screen and (max-width: 800px) {
    #vcomments-box {
      padding: 5px 0px;
    }
  }

  #vcomments-box #vcomments {
    background-color: #fff;
  }

  .v .vlist .vcard .vh {
    padding-right: 20px;
  }

  .v .vlist .vcard {
    padding-left: 10px;
  }
</style>

 
   
     
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2021
        <i class="ri-heart-fill heart_icon"></i> Chenyy
      </li>
    </ul>
    <ul>
      <li>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
        <script type="text/javascript" src='https://s9.cnzz.com/z_stat.php?id=1278069914&amp;web_id=1278069914'></script>
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="/images/ayer-side.svg" alt="ChenyyのBlog"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">文章</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags/book">收藏</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/about/me">关于我</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


    
  </div>
</body>

</html>